﻿using System;
using System.Collections;
using System.Collections.Generic;

namespace Simple3DRedactor.Domain
{
    public sealed class SceneView<TV> : IEnumerable<TV> where TV : IObjectView
    {
        private readonly IObjectViewFactory<TV> _factory;
        private readonly Dictionary<SceneObject, TV> _objectsViews = new Dictionary<SceneObject, TV>();

        public SceneView(Scene scene, IObjectViewFactory<TV> factory)
        {
            if (scene == null) throw new ArgumentNullException("scene");
            if (factory == null) throw new ArgumentNullException("factory");

            _factory = factory;

            Scene = scene;

            foreach (var sceneObject in Scene)
            {
                var view = sceneObject.CreateView(_factory);
                _objectsViews[sceneObject] = view;
                view.Change += OnChildChange;
            }

            Scene.ChildAdd += OnSceneObjectAdd;
            Scene.ChildRemove += OnSceneObjectRemove;
            Scene.ChildsClear += OnSceneChildsClear;
            Scene.ChildAssign += OnSceneObjectAssign;
        }

        public Scene Scene { get; private set; }

        public event Action<SceneView<TV>> Change;
        private void OnChange()
        {
            if (Change != null) Change(this);
        }

        public event Action<SceneView<TV>, TV> ChildChange;
        private void OnChildChange(IObjectView view)
        {
            if (ChildChange != null) ChildChange(this, (TV) view);
            OnChange();
        }

        public event Action<SceneView<TV>, TV> ChildAdd;
        private void OnChildAdd(TV view)
        {
            if (ChildAdd != null) ChildAdd(this, view);
            OnChange();
        }

        public event Action<SceneView<TV>, TV> ChildRemove;
        private void OnChildRemove(TV view)
        {
            if (ChildRemove != null) ChildRemove(this, view);
            OnChange();
        }

        public event Action<SceneView<TV>> ChildsClear;
        private void OnChildsClear()
        {
            if (ChildsClear != null) ChildsClear(this);
            OnChange();
        }

        public delegate void ChildAssignAction(SceneView<TV> scene, TV oldView, TV newView);
        public event ChildAssignAction ChildAssign;
        private void OnChildAssign(TV oldView, TV newView)
        {
            if (ChildAssign != null) ChildAssign(this, oldView, newView);
            OnChange();
        }

        public IEnumerator<TV> GetEnumerator()
        {
            return _objectsViews.Values.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        private void Clear()
        {
            foreach (var objectView in _objectsViews.Values)
            {
                objectView.Change -= OnChildChange;
                objectView.OnRemove();
            }

            _objectsViews.Clear();
        }

        private void OnSceneObjectAdd(Scene scene, SceneObject sceneObject)
        {
            var view = sceneObject.CreateView(_factory);
            _objectsViews[sceneObject] = view;
            view.Change += OnChildChange;

            OnChildAdd(view);
        }

        private void OnSceneObjectRemove(Scene scene, SceneObject sceneObject)
        {
            var view = _objectsViews[sceneObject];

            view.Change -= OnChildChange;
            view.OnRemove();

            _objectsViews.Remove(sceneObject);

            OnChildRemove(view);
        }

        private void OnSceneChildsClear(Scene scene)
        {
            Clear();

            OnChildsClear();
        }

        private void OnSceneObjectAssign(Scene scene, SceneObject oldObj, SceneObject newObj)
        {
            var oldView = _objectsViews[oldObj];

            oldView.Change -= OnChildChange;
            oldView.OnRemove();
            _objectsViews.Remove(oldObj);

            var newView = newObj.CreateView(_factory);
            _objectsViews[newObj] = newView;
            newView.Change += OnChildChange;

            OnChildAssign(oldView, newView);
        }
    }
}
